// Copyright 2010 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.


/**
 * @fileoverview The BackgroundScript represents the background portion of a
 * layer and has the same privileges as the extension's background code.  The
 * purpose of this class is to manage the injection of JavaScript into the
 * extension's background page, CSS is not recognized.  The code that is
 * injected is added through script tag additions and is not evaluated.
 *
 * The ContentScript class is owned and managed through the Layer class.  This
 * means that the data properties of the class are publically available.
 *
 * Public functions:
 *   shellExtension.BackgroundScript() - Constructs a new BackgroundScript
 *       object.  The initial state is BAD_OBJECT.
 *   shellExtension.BackgroundScript.create(data, baseUrl) - Parse the given
 *       data and returns a new BackgroundScript object.  This data will be
 *       validated as it could come from an external source.
 *
 * Public prototype functions for shellExtension.BackgroundScript:
 *   getStatus() - Returns the current status of the object and all of its
 *       dependencies.
 *   inject(id, log) - Injects the object's JavaScript into the background page
 *       if not already injected and is ready to be injected.
 *
 * @author jasonstredwick@google.com (Jason Stredwick)
 */


goog.provide('shellExtension.BackgroundScript');

goog.require('bite.common.log.Simple');
goog.require('bite.common.net.url');
goog.require('shellExtension.ScriptSet');
goog.require('shellExtension.manifest.script');
goog.require('shellExtension.manifest.script.background');
goog.require('shellExtension.status');



/**
 * This class contains information about a background script.  See the file
 * overview for more information.
 * @constructor
 */
shellExtension.BackgroundScript = function() {
  /**
   * This flag indicates that the BackgroundScript has already been injected
   * into the background page.
   * @type {boolean}
   */
  this.injected = false;

  /**
   * The ScriptSet contains all the scripts associated with this
   * BackgroundScript.
   * @type {!shellExtension.ScriptSet}
   */
  this.scriptSet = new shellExtension.ScriptSet();

  /**
   * The status of the BackgroundScript.
   * @type {shellExtension.status.Value}
   */
  this.status = shellExtension.status.Value.BAD_OBJECT;
};


/**
 * Create a new BackgroundScript with the given script data.  The new
 * BackgroundScript is created and returned regardless of its load
 * status; errors will be logged.  Always returning an object is important to
 * the functioning of the layer object.
 * @param {!Object} data Each component is optional and empty objects are
 *     acceptable. This data will be validated as it could come from an
 *     external source.
 * @param {string} baseUrl The base url of all relative linked scripts.
 * @param {!bite.common.log.Simple} log The log to record errors.
 * @return {!shellExtension.BackgroundScript} The new BackgroundScript.
 */
shellExtension.BackgroundScript.create = function(data, baseUrl, log) {
  var script = new shellExtension.BackgroundScript();

  // Validate data to ensure that is a valid object.
  if (!shellExtension.manifest.script.background.validate(data)) {
    var msgBase = 'ERROR (shellExtension.BackgroundScript.create): ';
    log.add(msgBase + 'Invalid data, expecting object.');
    return script;
  }

  var failed = false;
  var backgroundProperty = shellExtension.manifest.script.background.Property;
  for (var propertyKey in backgroundProperty) {
    var key = backgroundProperty[propertyKey];
    if (!(key in data)) {
      continue;
    }

    var dataValue = data[key];

    switch (key) {
      case backgroundProperty.JS_URL:
      case backgroundProperty.JS_CODE:
        key = /** @type {shellExtension.manifest.script.Property} */ (key);
        if (!script.addScripts_(baseUrl, dataValue, key, log)) {
          failed = true;
        }
        break;

      default:
        log.add('WARNING Unknown property in shellExtension.manifest.script.' +
                'background.Property (' + key + ').');
        break;
    }
  }

  // The script's data either failed to process or has been loaded.
  failed || (script.status = shellExtension.status.Value.LOADED);

  return script;
};


/**
 * Add scripts to the ScriptSet using information from data.
 * @param {string} baseUrl The base url of all relative linked scripts.
 * @param {!Array.<string>} data Each entry contains information related to a
 *     single script; either all raw code or all urls.  This data will be
 *     validated as it could come from an external source.
 * @param {shellExtension.manifest.script.Property} type The type of script.
 * @param {!bite.common.log.Simple} log The log to record errors.
 * @return {boolean} Whether or not processing was successful.
 * @private
 */
shellExtension.BackgroundScript.prototype.addScripts_ = function(baseUrl, data,
                                                                 type, log) {
  var msgBase = 'ERROR (shellExtension.BackgroundScript.addScripts_): ';

  // Validate data to ensure that it is array-like set of strings.
  if (!shellExtension.manifest.script.validateList(data)) {
    log.add(msgBase + 'Invalid data.');
    return false;
  }

  var scriptProperty = shellExtension.manifest.script.Property;
  var isUrl = type == scriptProperty.JS_URL;

  for (var i = 0, len = data.length; i < len; ++i) {
    var value = data[i];
    var id = isUrl ? bite.common.net.url.toAbsolute(value, baseUrl) : null;
    var scriptData = isUrl ? undefined : value;

    this.scriptSet.add(id, type, log, scriptData);
  }

  return true;
};


/**
 * Clears the script set's data; i.e. all loadable scripts are set to their
 * INIT state and pending.
 */
shellExtension.BackgroundScript.prototype.clearData = function() {
  this.scriptSet.clearData();
};


/**
 * Retrieves the status of the background script, which is BAD_OBJECT or the
 * status of its ScriptSet.
 * @return {shellExtension.status.Value} The status.
 */
shellExtension.BackgroundScript.prototype.getStatus = function() {
  if (this.status == shellExtension.status.Value.BAD_OBJECT) {
    return this.status;
  }

  return this.scriptSet.getStatus();
};


/**
 * Inject JavaScript code into the background page.  If the ScriptSet is not
 * loaded then nothing is injected.  Also note that background scripts can only
 * be loaded once.
 * @param {string} id An id given to identify the background script.
 * @param {!bite.common.log.Simple} log The log to record errors.
 */
shellExtension.BackgroundScript.prototype.inject = function(id, log) {
  // Do nothing if already injected.
  if (this.injected) {
    return;
  }

  var glob = this.scriptSet.getGlobs(log);
  // Return if nothing to insert.
  if (!glob || !glob[shellExtension.manifest.script.Property.JS_CODE]) {
    return;
  }

  var msgBase = 'ERROR (shellExtension.BackgroundScript.inject): ';

  var head = document.querySelector('head');
  if (!head) {
    log.add(msgBase + 'Failed to find head tag of background page.');
    return;
  }

  var element = document.createElement('script');
  if (!element) {
    log.add(msgBase + 'Failed to create a new script tag.');
    return;
  }

  var code = glob[shellExtension.manifest.script.Property.JS_CODE];
  // Wrap the javascript code in a try/catch block before injecting.  This
  // helps debugging injected code.
  var js = 'try {\n' + code + '\n} catch (error) {\n' +
           '  console.error("' + msgBase + ' [id=' + id + ']: " + error);\n' +
           '}\n';

  element.type = 'text/javascript';
  element.innerHTML = js;
  head.appendChild(element);

  this.injected = true;
};


/**
 * TODO (jasonstredwick): Add the ability to register callbacks for the
 * background script.  The following is the description for the file overview.
 *
 * The background script also offers layers the ability to hook into specific
 * events that can be applied them.  These events are
 *
 *     onDisable - Fired when the layer is disabled.
 *     onEnable - Fired when the layer is enabled.
 *     onInstall - Fired once the layer has loaded for the first time.
 *     onUninstall - Fired when the layer is being removed.
 *
 * The layer adds these hooks into their manifest by specifying the hook they
 * want and the function's full name.  The function specified for each hook
 * must be a global function and not belond to an object.  It also needs to
 * include the namespace the function exists in.  The background script will
 * parse this name and construct an array-like access to the function and
 * call it.  If it is not present then nothing happens.  The following example
 * shows the hook in the manifest and how it can be considered used internally.
 *
 *   Layer Manifest
 *       ...
 *       'onInstall': 'my.namespace.onInstallHandler',
 *       ...
 *
 *   Internal usage would be something like:
 *       var functionParts = manifest['onInstall'].split('.');
 *       var f = this;
 *       for (var part in functionParts) {
 *         f = f[part];
 *       }
 *       ...
 *       // Fire onInstall for layer
 *       f();
 *
 */

